package chalmers.game.subterranean.model.physics;

import chalmers.game.subterranean.util.ExactPosition;
import chalmers.game.subterranean.util.Position;
import chalmers.game.subterranean.model.world.IWorld;
import chalmers.game.subterranean.util.Direction;
import chalmers.game.subterranean.util.options.Variables;
import chalmers.game.subterranean.util.options.Speeds;
import chalmers.game.subterranean.util.options.WorldValues;
import java.util.ArrayList;
import java.util.Collections;
import lombok.Getter;
import lombok.Setter;

/**
 * Instances of this class represent a body that can move around in the world. For such bodies some
 * behaviors may not be altered and those are gathered here. What happens if the body bounces
 * however is to the class manipulating the body.
 * 
 * @author Alex Evert
 */
public class MovableBody implements IMovableBody {
  private @Getter
  @Setter
  double xVelocity;
  private @Getter
  @Setter
  double yVelocity;
  private @Setter
  IWorld world;
  private @Getter
  @Setter
  Position position;
  private double x;
  private double y;
  // The dimensions of the hitbox.
  private double height;
  private double width;
  private IPhysicsHandler physics;
  private @Setter
  double gravityfactor = 1.0;



  /**
   * Move the body by applying gravity and velocity see where it ends up.
   * 
   * @param physics The handler that is to determine physics for this round
   */
  @Override
  public void moveBody(IPhysicsHandler physics) {
    this.physics = physics;
    // Effect of gravity vary depending on factor.
    yVelocity -= Variables.GRAVITY * gravityfactor;
    normalizeSpeed();
    x = x + xVelocity;
    y = y + yVelocity;
    checkCrossed();
  }

  /**
   * If the body is set to move over MAXSPEED we normalize the velocity.
   */
  private void normalizeSpeed() {
    double norm = Math.abs(xVelocity) + Math.abs(yVelocity);
    if (norm > Speeds.MAXSPEED) {
      xVelocity = (xVelocity / norm) * Speeds.MAXSPEED;
      yVelocity = (yVelocity / norm) * Speeds.MAXSPEED;
    }
  }

  /**
   * Check if the body moved outside the square, if so try to move in that direction.
   */
  private void checkCrossed() {
    ArrayList<Direction> crossedDirections = new ArrayList<>();
    if (x > WorldValues.SQUAREWIDTH) {
      crossedDirections.add(Direction.EAST);
    }
    if (x < 0) {
      crossedDirections.add(Direction.WEST);
    }
    if (y > WorldValues.SQUAREWIDTH) {
      crossedDirections.add(Direction.NORTH);
    }
    if (y < 0) {
      crossedDirections.add(Direction.SOUTH);
    }

    // TODO: Make better ordering of crosses.
    //
    // This is easy,
    // we know previus x and y and just need to scale triangles
    // so that the new x resp. y coordinate sits on a square boarder
    // and then see wich of the two triangles have the shortest hypotenuse.
    if (Math.abs(x) > Math.abs(y)) {
      Collections.reverse(crossedDirections);
    }
    tryToCross(crossedDirections);

  }

  /**
   * Try to move to new square. If we are not allowed we bounce. otherwise we adjust our internal
   * position to the new square.
   */
  private void tryToCross(ArrayList<Direction> directions) {
    for (Direction direction : directions) {
      Position newPosition = world.nextPosition(position, direction);
      if (position.equals(newPosition)) {
        // If we hit something hard we bounce.
        pressAgainstBoarder(direction);
        physics.bounce(this, direction);
        // And don't continue crossing.
        // break;
      } else {
        if (Math.abs(y) > WorldValues.SQUAREWIDTH) {
          y = y % (WorldValues.SQUAREWIDTH);
        }
        if (y < 0) {
          y += WorldValues.SQUAREWIDTH;

        }
        if (Math.abs(x) > WorldValues.SQUAREWIDTH) {
          x = x % (WorldValues.SQUAREWIDTH);
        }
        if (x < 0) {
          x += WorldValues.SQUAREWIDTH;
        }

      }
      position = newPosition;
    }
  }

  // Simply make internal coordinates match boarders of square.
  private void pressAgainstBoarder(Direction direction) {
    switch (direction) {
      case EAST: {
        x = WorldValues.SQUAREWIDTH;
        break;
      }
      case NORTH: {
        y = WorldValues.SQUAREWIDTH;
        break;
      }
      case WEST: {
        x = 0;
        break;
      }
      case SOUTH: {
        y = 0;
        break;
      }
    }
  }

  /**
   * See if there is free space in a given direction.
   * 
   * @param direction direction we want to find free space in.
   * @return
   */
  @Override
  public boolean findFreeSpace(Direction direction) {
    return !world.nextPosition(position, direction).equals(position);
  }

  /**
   * Get the exact position of the body.
   * 
   * @return The exact position of the body.
   */
  @Override
  public ExactPosition getExactPosition() {
    return new ExactPosition(position, x, y);
  }
}
